<?php
class Pageley_Mapper_Gateway implements Pageley_Mapper {
	private $types;
	private $transaction;
	private $gateway;
	private $lastNewId;
	private $complete;
	private $readyToCommit;
	private $deleted;
    private $countBeforeQuery = true;
    private $queryTreshold = 750;

	public function __construct($gateway) {
		$this->types = new Pageley_Util_ArrayMap();
		$this->gateway = $gateway;
	}

	public function start() {
	    $this->transaction = $this->gateway->getTransaction(false);
        $this->complete = false;
        $this->lastNewId = 0;
        $this->readyToCommit = null;
        $this->types = new Pageley_Util_ArrayMap();
        $this->deleted = null;
        if(Pageley_Log::isDebugEnabled($this)) {
            Pageley_Log::debug($this, "Transaction started.");
        }
	}

	private function add($entity, $isDependentOnCurrent = true, $shouldValidate = true) {
	    $entries = $this->getEntries($entity->getType());
		$entry = null;
		$state = Pageley_Mapper_State::$CLEAN;
		$id = $entity->getId();
		$persistent = false;
		if($id === null) {
			$entry = $this->findEntryForEntity($entity, $entries);
			if($entry === null) {
                $id = "N" . ++$this->lastNewId;
                $state = Pageley_Mapper_State::$CREATED;
			}
		} else {
			$entry = $this->getEntryById($entity->getType(), $entity->getId());
			$persistent = true;
		}
	    if($shouldValidate && $state != Pageley_Mapper_State::$CLEAN) {
            $entity->validate();
        }
		if($entry === null) {
			$entry = new Pageley_Mapper_RegistryEntry($entity, $state);
			if($isDependentOnCurrent) {
				$entries->put($id, $entry);
			} else {
				$entries->prepend($id, $entry);
			}
			if($persistent) {
				$this->enableReferences($entity);
				$this->prepareLazyLoading($entity);
			}
		}
		return $entry->getEntity();
	}
	
	private function findEntryForEntity($entity, $entries) {
		$entryFound = null;
		foreach($entries as $entry) {
			if($entry->getEntity() === $entity) {
				$entryFound = $entry;
				break;
			}
		}
		return $entryFound;
	}

	private function enableReferences($entity) {
		foreach($entity->getType()->getAttributes() as $propertyName => $attribute) {
			if($attribute->isReference()) {
				$property = $entity->getProperty($propertyName);
				$property->setValueProvider($this);
			}
		}
	}

	private function prepareLazyLoading($entity) {
		foreach($entity->getType()->getAttributes() as $propertyName => $attribute) {
			if($attribute->isMultiple()) {
				$entity->setProperty($propertyName, new Pageley_Mapper_LazyValueListProperty(
					$entity, $propertyName, $attribute, $this));
			}
		}
	}

	public function register($entity) {
		$this->add($entity);
	}

	public function unregister($entity) {
		$entry = $this->getEntryById($entity->getType(), $entity->getId());
		$entry->setState(Pageley_Mapper_State::$DELETED);
	}

	public function commit() {
		$this->readyToCommit = new Pageley_Util_ArrayMap();
		$this->deleted = new Pageley_Util_ArrayMap();
		$this->registerUnregisteredEntities();
		$entriesSorted = $this->getEntriesSortedTopologically();
		$toDelete = new Pageley_Util_ArrayMap();
		foreach($entriesSorted as $entry) {
			if($entry->getState() == Pageley_Mapper_State::$CREATED) {
				$this->gateway->insert($entry->getEntity());
			} else if($entry->getState() == Pageley_Mapper_State::$UPDATED) {
				$this->gateway->update($entry->getEntity());
			} else if($entry->getState() == Pageley_Mapper_State::$DELETED) {
				$toDelete->add($entry);
			}
		}
		$toDelete->reverse();
		foreach($toDelete as $entry) {
			$this->gateway->delete($entry->getEntity());
		}
		$this->transaction->commit();
        $this->complete = true;
		if(Pageley_Log::isDebugEnabled($this)) {
		  Pageley_Log::debug($this, "Transaction committed.");
		}
	}

	private function getEntriesSortedTopologically() {
		$queue = new Pageley_Util_ArrayMap();
        $unqueued = new Pageley_Util_ArrayMap();
        $sortedEntries = new Pageley_Util_ArrayMap();
        foreach($this->types as $entries) {
            foreach($entries as $entry) {
            	if($this->hasReferences($entry)) {
            		$unqueued->add($entry);
            	} else {
            		$sortedEntries->add($entry);
            	}
            }
        }
        
        if($sortedEntries->size() == 0) {
        	throw new Pageley_Domain_RuntimeException("Cannot persist circular reference.");
        }
        $queue->push($sortedEntries->get(0));

        while(!$queue->isEmpty()) {
        	$entry = $queue->shift();
        	if(!$sortedEntries->containsValue($entry)) {
        	   $sortedEntries->add($entry);
        	}
        	$unqueued->removeValue($entry);
        	foreach($unqueued as $u) {
        		$entity = $u->getEntity();
        		$readyForQueue = true;
	        	foreach($entity->getType()->getAttributes() as $propertyName => $attribute) {
		            if($attribute->isReference()) {
		                $property = $entity->getProperty($propertyName);
		                if($property->isInitialized() && $property->getValue() != null) {
		                    $ref = $property->getValue();
		                    $refEntry = $this->getEntryByEntity($ref);
		                    if(!$sortedEntries->containsValue($refEntry) && !$queue->containsValue($refEntry)) {
		                    	$readyForQueue = false;
		                    	break;
		                    }
		                }
		            }
		        }
		        if($readyForQueue) {
		        	$queue->push($u);
		        }
        	}
        }
        return $sortedEntries;
	}

	private function hasReferences($entry) {
	    $hasReferences = false;
	    $entity = $entry->getEntity();
		foreach($entity->getType()->getAttributes() as $propertyName => $attribute) {
            if($attribute->isReference()) {
            	$property = $entity->getProperty($propertyName);
                if($property->isInitialized() && $property->getValue() != null) {
                    $hasReferences = true;
                }
                break;
            }
        }
        return $hasReferences;
	}

	private function registerUnregisteredEntities() {
		foreach($this->types as $entries) {
			foreach($entries as $entry) {
				$this->markReferencedEntitiesAsCreated($entry->getEntity());
				if($entry->getState() == Pageley_Mapper_State::$DELETED) {
					$this->markReferencedEntitiesAsDeleted($entry->getEntity());
				}
			}
		}
	}

	private function markReferencedEntitiesAsCreated($entity) {
		foreach($entity->getType()->getAttributes() as $propertyName => $attribute) {
			if($attribute->isReference()) {
				$property = $entity->getProperty($propertyName);
				if($property->isInitialized()) {
				    $this->setReadyToCommit($property->getValue(), false);
				}
			} else if($attribute->isMultiple() && $entity->getProperty($propertyName)->isInitialized()) {
				$values = $entity->get($propertyName);
				foreach($values as $value) {
					$this->setReadyToCommit($value, true);
				}
			}
		}
	}

	private function markReferencedEntitiesAsDeleted($entity) {
		foreach($entity->getType()->getAttributes() as $propertyName => $attribute) {
			if($attribute->isMultiple()) {
				$values = $entity->get($propertyName);
				foreach($values as $value) {
					$this->setDeleted($value);
				}
			}
		}
	}

	private function setReadyToCommit($entity, $isDependentOnCurrent) {
		if($entity != null) {
			$key = $this->entityKey($entity);
			if(!$this->readyToCommit->containsKey($key)) {
				$this->add($entity, $isDependentOnCurrent);
				$this->readyToCommit->put($key, $entity);
				$this->markReferencedEntitiesAsCreated($entity);
			}
		}
	}

	private function setDeleted($entity) {
		if($entity != null) {
			$key = $this->entityKey($entity);
			if(!$this->deleted->containsKey($key)) {
				$this->unregister($entity);
				$this->markReferencedEntitiesAsDeleted($entity);
			}
		}
	}

	private function entityKey($entity) {
		return $entity->getType() . "(" . $entity->getId() . ")";
	}

	public function rollback() {
		$this->transaction->rollback();
		$this->complete = true;
	    if(Pageley_Log::isDebugEnabled($this)) {
          Pageley_Log::debug($this, "Transaction rolled back.");
        }
	}

	public function find($type, $id) {
	    if(Pageley_Log::isDebugEnabled($this)) {
          Pageley_Log::debug($this, "Finding " . $type . " with ID " . $id);
        }
		$entity = $this->gateway->find($type, $id);
		return $entity !== null ? $this->add($entity, true, false) : null;
	}

	private function getEntries($type) {
		$typeName = $type->__toString();
		$entries = $this->types->get($typeName);
		if($entries === null) {
			$entries = new Pageley_Util_ArrayMap();
			$this->types->put($typeName, $entries);
		}
		return $entries;
	}

	private function getEntryById($type, $id) {
		$entries = $this->getEntries($type);
		return $entries->get($id);
	}

    private function getEntryByEntity($entity) {
        $entries = $this->getEntries($entity->getType());
        $result = null;
        foreach($entries as $entry) {
        	if($entry->getEntity() == $entity) {
        		$result = $entry;
        		break;
        	}
        }
        return $result;
    }

	public function query($query) {
	    if(Pageley_Log::isDebugEnabled($this)) {
             Pageley_Log::debug($this, "Executing " . $query . ".");
        }
        $count = -1;
        if($this->countBeforeQuery) {
            $count = $this->gateway->count($query);
            if($count > $this->queryTreshold) {
                throw new Pageley_Mapper_TresholdExceededException("Number of results (" . $count . ") larger than treshold (" . 
                    $this->queryTreshold . ") for query: " . $query);
            }
        }
        $entities = new Pageley_Util_ArrayMap();
        if($count != 0) {
            $entities = $this->gateway->query($query);
        }
		foreach($entities as $key => $entity) {
			$entities->put($key, $this->add($entity, true, false));
		}
		return $entities;
	}

	public function isComplete() {
		return $this->complete;
	}
    
    public function setCountBeforeQuery($countBeforeQuery) {
        $this->countBeforeQuery = $countBeforeQuery;
    }
    
    public function setQueryTreshold($queryTreshold) {
        $this->queryTreshold = $queryTreshold;
    }
}